﻿using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using System;

namespace WaterExample
{
    /// <summary>
    /// Initialize a new instance of Terrain.
    /// Creates a Terrain based on a gray picture.
    /// From the book "3D Graphics with XNA Game Studio 4.0"
    /// </summary>
    public class Terrain
    {
        #region Variables
        private VertexPositionNormalTexture[] _vertices; // Vertex array
        private readonly VertexBuffer _vertexBuffer; // Vertex buffer
        private int[] _indices; // Index array
        private readonly IndexBuffer _indexBuffer; // Index buffer

        private float[,] _heights; // Array of vertex heights
        private readonly float _height; // Maximum height of terrain
        private readonly float _cellSize; // Distance between vertices on x and z axes
        private readonly float _textureTiling;

        private readonly int _width; // Number of vertices on x and z axes
        private readonly int _length; // Number of vertices on x and z axes
        private readonly int _nVertices; // Number of vertices and indices
        private readonly int _nIndices; // Number of vertices and indices

        private readonly Effect _effect; // Effect used for rendering
        private readonly Game _game;

        //Textures
        private readonly Texture2D _baseTexture;
        private readonly Texture2D _heightMap; // Heightmap texture
        private Texture2D _rTexture, _bTexture, _gTexture, _weightMap;

        public Matrix World = Matrix.Identity;

        #region Properties
        public Texture2D RTexture
        {
            get { return _rTexture; }
            set { _rTexture = value; }
        }

        public Texture2D BTexture
        {
            get { return _bTexture; }
            set { _bTexture = value; }
        }

        public Texture2D GTexture
        {
            get { return _gTexture; }
            set { _gTexture = value; }
        }

        public Texture2D WeightMap
        {
            get { return _weightMap; }
            set { _weightMap = value; }
        }
        #endregion
        #endregion

        /// <summary>
        /// Initialize a new instance of Terrain.
        /// </summary>
        public Terrain(float cellSize, float height, float textureTiling, Game game)
        {
            _baseTexture = game.Content.Load<Texture2D>("Textures/grass");
            _heightMap = game.Content.Load<Texture2D>("Textures/terrain");
            _weightMap = game.Content.Load<Texture2D>("Textures/weightMap");
            _rTexture = game.Content.Load<Texture2D>("Textures/sand");
            _gTexture = game.Content.Load<Texture2D>("Textures/rock");
            _bTexture = game.Content.Load<Texture2D>("Textures/snow"); 

            _textureTiling = textureTiling;
            _width = _heightMap.Width;
            _length = _heightMap.Height;
            _cellSize = cellSize;
            _height = height;

            _game = game;

            _effect = game.Content.Load<Effect>("Shaders/TerrainEffect");

            // 1 vertex per pixel
            _nVertices = _width * _length;

            // (Width-1) * (Length-1) cells, 2 triangles per cell, 3 indices per triangle
            _nIndices = (_width - 1) * (_length - 1) * 6;

            _vertexBuffer = new VertexBuffer(game.GraphicsDevice, typeof(VertexPositionNormalTexture),
                _nVertices, BufferUsage.WriteOnly);

            _indexBuffer = new IndexBuffer(game.GraphicsDevice, IndexElementSize.ThirtyTwoBits,
                _nIndices, BufferUsage.WriteOnly);

            GetHeights();
            CreateVertices();
            CreateIndices();
            GenNormalsBinormalsTangents();

            _vertexBuffer.SetData(_vertices);
            _indexBuffer.SetData(_indices);
        }
        /// <summary>
        /// Gets the y-Position  of the specific point in the terrain and returns the angle in this area.
        /// </summary>
        /// <param name="x">X-Coordinate of point</param>
        /// <param name="z">Z-Coordinate of point</param>
        /// <param name="steepness">Angle at this point of the terrain</param>
        /// <returns>Angle of terrain at this point</returns>
        public float GetHeightAtPosition(float x, float z, out float steepness)
        {
            //Clamp coordinates to locations on terrain
            x = MathHelper.Clamp(x, (-_width / 2f) * _cellSize, (_width / 2f) * _cellSize);
            z = MathHelper.Clamp(z, (-_length / 2f) * _cellSize, (_length / 2f) * _cellSize);

            //Map from (-Width/2 -> Width/2, -Length/2 -> Lenth/2) to (0->Width, 0->Length)
            x += (_width / 2f) * _cellSize;
            z += (_length / 2f) * _cellSize;

            //Map to cell coordinates
            x /= _cellSize;
            z /= _cellSize;

            //Truncate coordinates to get coordinates of top left cell vertex
            var x1 = (int)x;
            var z1 = (int)z;

            //Try to get coordinates of bottom right cell vertex
            var x2 = x1 + 1 == _width ? x1 : x1 + 1;
            var z2 = z1 + 1 == _length ? z1 : z1 + 1;

            //Get the heights at the two corners of the cell
            var h1 = _heights[x1, z1];
            var h2 = _heights[x2, z2];

            //Dtermine steepness (angle between heigher and lower vertex of cell)
            steepness = (float)Math.Atan(Math.Abs((h1 - h2)) / (_cellSize * Math.Sqrt(2)));

            //Fint the average of the amounts lost from coordinates during truncation above
            var leftOver = ((x - x1) + (z - z1)) / 2f;

            //Interpolate between the corner vertices' heights
            return MathHelper.Lerp(h1, h2, leftOver);
        }

        /// <summary>
        /// Gets the height of each vertex out of the height map.
        /// </summary>
        private void GetHeights()
        {
            // Extract pixel data
            var heightMapData = new Color[_width * _length];
            _heightMap.GetData(heightMapData);

            // Create heights[,] array
            _heights = new float[_width, _length];

            // For each pixel
            for (int y = 0; y < _length; y++)
                for (int x = 0; x < _width; x++)
                {
                    // Get color value (0 - 255)
                    float amt = heightMapData[y * _width + x].R;

                    // Scale to (0 - 1)
                    amt /= 255.0f;

                    // Multiply by max height to get final height
                    _heights[x, y] = amt * _height;
                }
        }

        /// <summary>
        /// Create for each pixel in the height map a vertex.
        /// </summary>
        private void CreateVertices()
        {
            _vertices = new VertexPositionNormalTexture[_nVertices];

            // Calculate the position offset that will center the terrain at (0, 0, 0)
            var offsetToCenter = -new Vector3((_width / 2.0f) * _cellSize,
                200, (_length / 2.0f) * _cellSize);

            // For each pixel in the image
            for (var z = 0; z < _length; z++)
                for (var x = 0; x < _width; x++)
                {
                    // Find position based on grid coordinates and height in heightmap
                    var position = new Vector3(x * _cellSize,
                        _heights[x, z], z * _cellSize) + offsetToCenter;

                    // UV coordinates range from (0, 0) at grid location (0, 0) to 
                    // (1, 1) at grid location (width, length)
                    var uv = new Vector2((float)x / _width, (float)z / _length);

                    // Create the vertex
                    //vertices[z * width + x] = new TerrainVertex(
                    //    position, Vector3.Zero, uv, Vector3.Zero, Vector3.Zero);
                    _vertices[z * _width + x] = new VertexPositionNormalTexture(
                        position, Vector3.Zero, uv);
                }
        }

        /// <summary>
        /// Create the indices.
        /// </summary>
        private void CreateIndices()
        {
            _indices = new int[_nIndices];

            int i = 0;

            // For each cell
            for (int x = 0; x < _width - 1; x++)
                for (int z = 0; z < _length - 1; z++)
                {
                    // Find the indices of the corners
                    int upperLeft = z * _width + x;
                    int upperRight = upperLeft + 1;
                    int lowerLeft = upperLeft + _width;
                    int lowerRight = lowerLeft + 1;

                    // Specify upper triangle
                    _indices[i++] = upperLeft;
                    _indices[i++] = upperRight;
                    _indices[i++] = lowerRight;

                    // Specify lower triangle
                    _indices[i++] = upperLeft;
                    _indices[i++] = lowerRight;
                    _indices[i++] = lowerLeft;
                }
        }

        /// <summary>
        /// Calculates the normals, binormals and tangents of the vertices.
        /// </summary>
        private void GenNormalsBinormalsTangents()
        {
            // For each triangle
            for (int i = 0; i < _nIndices; i += 3)
            {
                // Find the position of each corner of the triangle
                Vector3 v1 = _vertices[_indices[i]].Position;
                Vector3 v2 = _vertices[_indices[i + 1]].Position;
                Vector3 v3 = _vertices[_indices[i + 2]].Position;

                // Cross the vectors between the corners to get the normal
                Vector3 normal = Vector3.Cross(v1 - v3, v1 - v2);
                normal.Normalize();
                                                 
                ////Calculate Binormal and Tangent
                //Vector2 w1 = vertices[indices[i]].TextureCoordinate;
                //Vector2 w2 = vertices[indices[i + 1]].TextureCoordinate;
                //Vector2 w3 = vertices[indices[i + 2]].TextureCoordinate;

                //float x1 = v2.X - v1.X;
                //float x2 = v3.X - v1.X;
                //float y1 = v2.Y - v1.Y;
                //float y2 = v3.Y - v1.Y;
                //float z1 = v2.Z - v1.Z;
                //float z2 = v3.Z - v1.Z;

                //float s1 = w2.X - w1.X;
                //float s2 = w3.X - w1.X;
                //float t1 = w2.Y - w1.Y;
                //float t2 = w3.Y - w1.Y;

                //float r = 1.0f / (s1 * t2 - s2 * t1);
                //Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
                //Vector3 tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);

                //// Gram-Schmidt orthogonalize  
                //Vector3 tangent = sdir - normal * Vector3.Dot(normal, sdir);
                //tangent.Normalize();

                //// Calculate handedness (here maybe you need to switch >= with <= depend on the geometry winding order)  
                //float tangentdir = (Vector3.Dot(Vector3.Cross(normal, sdir), tdir) >= 0.0f) ? 1.0f : -1.0f;
                //Vector3 binormal = Vector3.Cross(normal, tangent) * tangentdir;

                // Add the influence of the normal, binormal and tangent to each vertex in the
                // triangle
                _vertices[_indices[i]].Normal += normal;
                //vertices[indices[i]].Binormal += binormal;
                //vertices[indices[i]].Tangent += tangent;
                _vertices[_indices[i + 1]].Normal += normal;
                //vertices[indices[i + 1]].Binormal += binormal;
                //vertices[indices[i + 1]].Tangent += tangent;
                _vertices[_indices[i + 2]].Normal += normal;
                //vertices[indices[i + 2]].Binormal += binormal;
                //vertices[indices[i + 2]].Tangent += tangent;
            }

            // Average the influences of the triangles touching each
            // vertex
            for (int i = 0; i < _nVertices; i++)
            {
                _vertices[i].Normal.Normalize();
                //vertices[i].Binormal.Normalize();
                //vertices[i].Tangent.Normalize();
            }
        }

        /// <summary>
        /// Draws the terrain.
        /// </summary>
        /// <param name="view">View matrix of camera.</param>
        /// <param name="projection">Projection matrix of camera.</param>
        public void Draw(Matrix view, Matrix projection)
        {
            _game.GraphicsDevice.BlendState = BlendState.Opaque;
            _game.GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            _game.GraphicsDevice.SetVertexBuffer(_vertexBuffer);
            _game.GraphicsDevice.Indices = _indexBuffer;

            _effect.Parameters["World"].SetValue(World);
            _effect.Parameters["View"].SetValue(view);
            _effect.Parameters["Projection"].SetValue(projection);
            _effect.Parameters["BaseTexture"].SetValue(_baseTexture);
            _effect.Parameters["TextureTiling"].SetValue(_textureTiling);
            _effect.Parameters["RTexture"].SetValue(_rTexture);
            _effect.Parameters["GTexture"].SetValue(_gTexture);
            _effect.Parameters["BTexture"].SetValue(_bTexture);
            _effect.Parameters["WeightMap"].SetValue(_weightMap);
            _effect.Parameters["LightDirection"].SetValue(Game1.LightDirection);
            _effect.Parameters["LightColor"].SetValue(Game1.LightColor);
            _effect.Parameters["CameraPosition"].SetValue(Camera.Position);

            _effect.Techniques[0].Passes[0].Apply();

            foreach (EffectPass effectPass in _effect.CurrentTechnique.Passes)
            {
                effectPass.Apply();
                _game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                     _nVertices, 0, _nIndices/3);                
            }

            _game.GraphicsDevice.SetVertexBuffer(null);
            _game.GraphicsDevice.Indices = null;
        }
    }
}



